/*
 * Copyright 2011-2012 David Crosson
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package fr.janalyse.series.view

import collection.JavaConversions._
import fr.janalyse.series._
import java.awt.image.BufferedImage
import org.jfree.chart.plot.Plot
import org.jfree.chart.JFreeChart
import org.jfree.chart.ChartFactory
import org.jfree.chart.plot.XYPlot
import java.awt.Color
import java.awt.RenderingHints
import org.jfree.chart.axis.NumberAxis
import org.jfree.chart.axis.AxisLocation
import org.jfree.data.xy.XYDataset
import org.jfree.chart.labels.XYToolTipGenerator
import org.jfree.data.DomainOrder
import org.jfree.data.general.DatasetChangeListener
import org.jfree.data.general.DatasetGroup
import java.awt.BasicStroke
import java.awt.geom.Ellipse2D
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer
import org.jfree.chart.ChartUtilities
import java.io.File
import java.text.NumberFormat
import java.text.FieldPosition
import java.text.ParsePosition
import scala.xml.Elem
import scala.xml.XML
import java.io.InputStream
import scala.util.matching.Regex

// =========================================================================================================================

class ColorPool {
  private def color(r:Int, g:Int, b:Int) = new Color(r,g,b)
  private val basecolors = {
    val (d,m,f)=(80, 160, 220)
    List(
		color(  d, d, d), // 0
        color(  m, 0, 0), // 1
        color(  0, m, 0), // 2
        color(  0, 0, m), // 3
        color(200,100,0), // 4
        color(  m, 0, m), // 5
        color(  0, m, m), // 6
        color(  m, m, 0), // 7
        color(  100, 190, 100), // 8
        color(  0, d, f), // 9
        color(  f, 0, d), // 10
        color(  m, d, m), // 11
        color(  d, 0, f) // 12
    )
  }
  private var generatedCounter=0
  private def generateColor() = {
    val rest = generatedCounter%basecolors.size
    val base = generatedCounter/basecolors.size
    var color = basecolors.get(rest)
    if (base%3==0) color=color.brighter
    if (base%3==2) color=color.brighter.brighter
    generatedCounter+=1
    color
  }

  var colorCache=List.empty[Color]
  
  def take():Color = {
    colorCache match {
      case Nil => generateColor()
      case h::t => colorCache = t ; h
    }
  }
  
  def giveback(c:Color) = colorCache = c :: colorCache

}

// =========================================================================================================================


class ScaleNumberFormat(scale:Scale) extends NumberFormat  {
    val defaultNumberFormat:NumberFormat = NumberFormat.getInstance()
	defaultNumberFormat.setGroupingUsed(false)
    defaultNumberFormat.setMaximumFractionDigits(1)
    
    override def format(number:Double, toAppendTo:StringBuffer, pos:FieldPosition):StringBuffer =
        defaultNumberFormat.format(scale.convert(number), toAppendTo, pos)

    override def format(number:Long, toAppendTo:StringBuffer, pos:FieldPosition):StringBuffer =
        defaultNumberFormat.format(scale.convert(number), toAppendTo, pos)

    override def parse(source:String, parsePosition:ParsePosition) = 
        defaultNumberFormat.parse(source, parsePosition)
}


object ScaleManager {
  def apply() = new ScaleManager(None)
  def apply(data:String) = new ScaleManager(Some(lessStrictXML.load(data)))
  def apply(in:InputStream) = new ScaleManager(Some(lessStrictXML.load(in)))
  def apply(file:File) = new ScaleManager(Some(lessStrictXML.loadFile(file)))
  def apply(root:Elem) = new ScaleManager(Some(root))
  def lessStrictXML = {
	val f = javax.xml.parsers.SAXParserFactory.newInstance()
	f.setNamespaceAware(false)
	f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    XML.withSAXParser(f.newSAXParser())
  }
}

case class ScaleConfig(
    group:String,
    name:String,
    pattern:Regex,
    scaletype:String,
    normalize:Double=1d,
    unitName:Option[String]=None,
    unitDesc:Option[String]=None

    )

case class ScaleManager(xmlcfg:Option[Elem]) {
  val scalesSpec = xmlcfg map {xroot =>
    for (xgroup <- xroot \ "scalegroup";
         xscale <- xgroup \ "scale") yield {
      ScaleConfig(
          group     = (xgroup \ "@name").text,
          name      = (xscale \ "@name").text,
          pattern   = ("(?i)"+(xscale \ "@pattern").text).r,
          scaletype = (xscale.attribute("type")).map(_.text).getOrElse("scale"),
          normalize = xscale.attribute("normalize").map(_.text.toDouble).getOrElse(1d),
          unitName  = xscale.attribute("unitname").map(_.text),
          unitDesc  = xscale.attribute("unitdesc").map(_.text)
      )
    }
  }

  var rangeAxisCount=0
  
  def nextRangeAxisNumber() = {
    rangeAxisCount+=1
    rangeAxisCount-1
  }
  
  lazy val default = new ScaleDefault(nextRangeAxisNumber())
  
  var currentScales = Map.empty[ScaleConfig, Scale]
  
  def updateScales(seriesViewList:Iterable[SeriesView]) {
    val seriesViewByScales = seriesViewList groupBy {_.scale}
    for((scale, svList)<-seriesViewByScales) {
    	val mins = svList map {_.getMin()}
	    val maxs = svList map {_.getMax()}
	    scale.bounds = Some(ScaleBounds(mins.min, maxs.max))
	  }  
  }
  
  def scalesFor(series:Series[Cell]) : Scale = {
    val found = scalesSpec flatMap {sp =>
      sp find {sc => (sc.pattern findFirstIn series.name).isDefined }
    }
    for(sc <- found if (! currentScales.contains(sc))) {
      currentScales += sc -> {
        sc.scaletype.toLowerCase() match {
        	case "scaleautobytessize"|"autobytessize"|"scaleautobytescount"|"autobytescount" =>
        	  ScaleAutoBytesCount(nextRangeAxisNumber(), Some(sc.name), sc.normalize)
        	case "scaleautobytesrate"|"autobytesrate" =>
        	  ScaleAutoBytesRate(nextRangeAxisNumber(), Some(sc.name), sc.normalize)
        	case "scaleautocount"|"autocount" =>
        	  ScaleAutoCount(nextRangeAxisNumber(), Some(sc.name), sc.normalize)
        	case "scaleautorate"|"autorate" =>
        	  ScaleAutoRate(nextRangeAxisNumber(), Some(sc.name), sc.normalize)
        	case "scaleautoduration"|"autoduration" =>
        	  ScaleAutoDuration(nextRangeAxisNumber(), Some(sc.name), sc.normalize)
        	case "scale" =>
        	  val nsc = ScaleDefault(nextRangeAxisNumber(), Some(sc.name), normalize=sc.normalize)
        	  nsc.unitName = sc.unitName    // TODO - BAD make scales immutable
        	  nsc.unitDesc = sc.unitDesc    // TODO - BAD make scales immutable
        	  nsc
        }
      }
    }
    found map {sc => currentScales.getOrElse(sc, default) } getOrElse default
  }
}

case class ScaleBounds(lower:Double, upper:Double)

trait Scale {
  val rangeAxisIndex:Int
  val name:Option[String]=None
  val normalize:Double=1d
  var unit:Double=1d                 // TODO - BAD make scales immutable
  var unitName:Option[String]=None   // TODO - BAD make scales immutable
  var unitDesc:Option[String]=None   // TODO - BAD make scales immutable
  def fullName = name map {_+unitDesc.map(" (%s)".format(_)).getOrElse("")}
  def convert[N<%Number](value:N) = value.doubleValue() * normalize / unit
  protected var currentBounds:Option[ScaleBounds]=None
  def bounds:Option[ScaleBounds] = currentBounds
  def bounds_=(newBounds:Option[ScaleBounds]) { currentBounds = newBounds}
}


case class ScaleDefault(
    override val rangeAxisIndex:Int,
    override val name:Option[String]=None,
    override val normalize:Double=1d) extends Scale {
}


case class ScaleAutoCount(
    override val rangeAxisIndex:Int,
    override val name:Option[String]=Some("Count"), 
    override val normalize:Double=1d) extends Scale {
  
    override def bounds_=(newBounds:Option[ScaleBounds]) {
        super.bounds = newBounds        
        bounds map {b => (b.upper - b.lower)*normalize match {
          	case x if x < 1000d =>                           unitDesc=None ; unitName=None ;          unit=1d
          	case x if x < 1000d*1000 =>          unitDesc=Some("thousands"); unitName=Some("x10^3");  unit=1000d
          	case x if x < 1000d*1000*1000 =>      unitDesc=Some("millions"); unitName=Some("x10^6");  unit=1000d*1000
          	case x if x < 1000d*1000*1000*1000 => unitDesc=Some("billions"); unitName=Some("x10^9");  unit=1000d*1000*1000
          	case _ =>                            unitDesc=Some("trillions"); unitName=Some("x10^12"); unit=1000d*1000*1000*1000
        	}
        }
    }
}

case class ScaleAutoDuration(
    override val rangeAxisIndex:Int,
    override val name:Option[String]=Some("Duration"), 
    override val normalize:Double=1d) extends Scale {
  
    override def bounds_=(newBounds:Option[ScaleBounds]) {
        super.bounds = newBounds        
        bounds map {b => (b.upper - b.lower)*normalize match {
          	case x if x < 1000d =>    unitDesc=Some("milliseconds"); unitName=Some("ms"); unit=1d
          	case x if x < 60d*1000 =>      unitDesc=Some("seconds"); unitName=Some("s") ; unit=1000d
          	case x if x < 60d*60*1000 =>   unitDesc=Some("minutes"); unitName=Some("mn"); unit=60d*1000
          	case x if x < 60d*60*1000*24 =>  unitDesc=Some("hours"); unitName=Some("h") ; unit=60d*60*1000
          	case x if x < 60d*60*1000*24*7 => unitDesc=Some("days"); unitName=Some("d") ; unit=60d*60*1000*24
          	case _ =>                        unitDesc=Some("weeks"); unitName=Some("w") ; unit=60d*60*1000*24*7
        	}
        }
    }
}

case class ScaleAutoRate(
    override val rangeAxisIndex:Int,
    override val name:Option[String]=Some("Rate"), 
    override val normalize:Double=1d) extends Scale {
  
    override def bounds_=(newBounds:Option[ScaleBounds]) {
        super.bounds = newBounds        
        bounds map {b => (b.upper - b.lower)*normalize match {
          	case x if x > 1000d =>      unitDesc=Some("each millisecond"); unitName=Some("/ms"); unit=1000d
          	case x if x > 1d/60/1000d =>     unitDesc=Some("each second"); unitName=Some("/s") ; unit=1d
          	case x if x > 1d/60/60/1000 =>   unitDesc=Some("each minute"); unitName=Some("/mn"); unit=1d/60/1000
          	case x if x > 1d/60/60/1000/24 =>  unitDesc=Some("each hour"); unitName=Some("/h") ; unit=1d/60/60/1000
          	case x if x > 1d/60/60/1000/24/7 => unitDesc=Some("each day"); unitName=Some("/d") ; unit=1d/60/60/1000/24
          	case _ =>                          unitDesc=Some("each week"); unitName=Some("/w") ; unit=1d/60/60/1000/24/7
        	}
        }
    }
}

case class ScaleAutoBytesCount (
    override val rangeAxisIndex:Int,
    override val name:Option[String]=Some("BytesCount"), 
    override val normalize:Double=1d) extends Scale {
  
    override def bounds_=(newBounds:Option[ScaleBounds]) {
        super.bounds = newBounds        
        bounds map {b => (b.upper - b.lower)*normalize match {
          	case x if x < 1024d =>               unitDesc=Some("bytes"); unitName=Some("b") ;  unit=1d
          	case x if x < 1024d*1024 =>      unitDesc=Some("kilobytes"); unitName=Some("kb") ; unit=1024d
          	case x if x < 1024d*1024*1024 => unitDesc=Some("megabytes"); unitName=Some("mb") ; unit=1024d*1024
          	case _ =>                        unitDesc=Some("gigabytes"); unitName=Some("gb") ; unit=1024d*1024*1024
        	}
        }
    }
}

case class ScaleAutoBytesRate (
    override val rangeAxisIndex:Int,
    override val name:Option[String]=Some("BytesRate"), 
    override val normalize:Double=1d) extends Scale {
  
    override def bounds_=(newBounds:Option[ScaleBounds]) {
        super.bounds = newBounds        
        bounds map {b => (b.upper - b.lower)*normalize match {
          	case x if x < 1024d =>               unitDesc=Some("bytes/second"); unitName=Some("b/s") ;  unit=1d
          	case x if x < 1024d*1024 =>      unitDesc=Some("kilobytes/second"); unitName=Some("kb/s") ; unit=1024d
          	case x if x < 1024d*1024*1024 => unitDesc=Some("megabytes/second"); unitName=Some("mb/s") ; unit=1024d*1024
          	case _ =>                        unitDesc=Some("gigabytes/second"); unitName=Some("gb/s") ; unit=1024d*1024*1024
        	}
        }
    }
}




// =========================================================================================================================


object SeriesView {
	val basicStroke = new BasicStroke(1f)
	val markedStroke = new BasicStroke(3f)
	val outlineBasicStroke = new BasicStroke(1.0f)
	val outlineMarkedStroke = new BasicStroke(2.0f)
    val shape = new Ellipse2D.Double(-2D, -2D, 4D, 4D);
}

case class SeriesView(series:Series[Cell], datasetIndex:Int, scale:Scale, color:Color=Color.blue) extends XYDataset {

	val renderer = new XYLineAndShapeRenderer() {
	  setDrawOutlines(true)
	  setUseFillPaint(true)
	  setSeriesShapesVisible(0, true)
      setSeriesShapesFilled(0, true)
      setSeriesFillPaint(0, Color.white)
      setSeriesPaint(0, color)
      setSeriesStroke(0, SeriesView.basicStroke)
      setSeriesOutlineStroke(0, SeriesView.outlineBasicStroke)
      setSeriesShape(0, SeriesView.shape)
	}
  
    var datasetGroup:DatasetGroup=_

    override def getDomainOrder() = DomainOrder.NONE

    override def getItemCount(seriesIndex:Int):Int = series.size

    override def getX(seriesIndex:Int, item:Int):Number = series.get(item).time

    override def getXValue(seriesIndex:Int, item:Int):Double = series.get(item).time

    override def getY(seriesIndex:Int, item:Int):Number = getValue(item)

    override def getYValue(seriesIndex:Int, item:Int):Double = getValue(item)
    
    override def getSeriesCount():Int = 1

    override def getSeriesKey(seriesIndex:Int):Comparable[String] = series.alias

    override def indexOf(seriesKey:Comparable[_]):Int = 0

    override def addChangeListener(listener:DatasetChangeListener ) {}

    override def removeChangeListener(listener:DatasetChangeListener ) {}

    override def getGroup():DatasetGroup = datasetGroup
    
    override def setGroup(group:DatasetGroup) {this.datasetGroup = group}

    def getValue(item:Int):Double = series.get(item).value

    def getMin():Double = series.stat.min
    
    def getMax():Double = series.stat.max
}


// =========================================================================================================================

object ChartConfig {
  implicit def defaultChartConfig:ChartConfig = ChartConfig()
}

case class ChartConfig(
    scaleManager:()=>ScaleManager=()=>ScaleManager(),
    destDir:Option[File]=None,
    width:Int=800,
    height:Int=450)


object Chart {
  def apply(seriesArray:Series[Cell]*)(implicit chartConfig:ChartConfig) = 
    new Chart(seriesArray.toIterable)(chartConfig)
  def apply(seriesList:Iterable[Series[Cell]])(implicit chartConfig:ChartConfig) = 
    new Chart(seriesList)(chartConfig)
  def apply(title:String, seriesArray:Series[Cell]*)(implicit chartConfig:ChartConfig) = 
    new Chart(seriesArray.toIterable, Some(title))(chartConfig)
  def apply(title:String, seriesList:Iterable[Series[Cell]])(implicit chartConfig:ChartConfig) = 
    new Chart(seriesList, Some(title))(chartConfig)
}

class Chart(seriesList:Iterable[Series[Cell]], title:Option[String]=None)(implicit chartConfig:ChartConfig) {
  import AxisLocation.{BOTTOM_OR_LEFT => leftSide, BOTTOM_OR_RIGHT => rightSide}
  import RenderingHints._
  
  //private val scaleManager = ScaleManager()
  private val colorPool = new ColorPool()
  private val jchart = ChartFactory.createTimeSeriesChart(null, null, null, null, false , true, false)
  private val jplot  = jchart.getPlot().asInstanceOf[XYPlot]
  private val jfree = new JFreeChart(jplot)
  title foreach {jfree.setTitle(_)}
  
  val scaleManager = chartConfig.scaleManager()
  
  jplot.setBackgroundPaint(new Color(220,220,220));
  jplot.setDomainGridlinePaint(Color.white);
  jplot.setRangeGridlinePaint(Color.white);

  val hints =
    Map(KEY_ANTIALIASING        -> VALUE_ANTIALIAS_ON,
  		KEY_ALPHA_INTERPOLATION -> VALUE_ALPHA_INTERPOLATION_SPEED,
        KEY_COLOR_RENDERING     -> VALUE_COLOR_RENDER_SPEED,
        KEY_DITHERING           -> VALUE_DITHER_DISABLE,
        KEY_INTERPOLATION       -> VALUE_INTERPOLATION_NEAREST_NEIGHBOR,
        KEY_RENDERING           -> VALUE_RENDER_SPEED,
        KEY_TEXT_ANTIALIASING   -> VALUE_TEXT_ANTIALIAS_OFF,
        KEY_STROKE_CONTROL      -> VALUE_STROKE_PURE)
          
  for( (k,v) <- hints) jchart.getRenderingHints().put(k,v)
  jchart.setAntiAlias(true)
  jchart.setTextAntiAlias(true)
  
  private var seriesViewList     = seriesList.zipWithIndex map {case (s,i) => SeriesView(s,i, scaleManager.scalesFor(s), colorPool.take())}
  private var seriesViewByScales = seriesViewList groupBy {sv => sv.scale}
  private var scaleLocation      = seriesViewByScales.keys.zipWithIndex map {case (sv,i) => sv -> (if (i%2==0) leftSide else rightSide)} toMap
  private var seriesViewScale    = seriesViewList map {sv => sv -> sv.scale} toMap
  
  // --- Initializing scales
  scaleManager.updateScales(seriesViewList)
  
  // --- Creating JFreeChart Scales
  for(scale <- seriesViewByScales.keys) {
	val axis = scale.fullName map {n => new NumberAxis(n)} getOrElse new NumberAxis()
	axis.setNumberFormatOverride(new ScaleNumberFormat(scale))
	axis.setAutoRange(true)
	axis.setAutoRangeIncludesZero(false)
	jplot.setRangeAxis(scale.rangeAxisIndex, axis)
	jplot.setRangeAxisLocation(scale.rangeAxisIndex, scaleLocation.get(scale) getOrElse leftSide)
  }

  // --- Adding datasets to JFreeChart
  for(sv <- seriesViewList) {
	jplot.setRenderer(sv.datasetIndex, sv.renderer)
	jplot.setDataset(sv.datasetIndex, sv)
	jplot.mapDatasetToRangeAxis(sv.datasetIndex, seriesViewScale.get(sv).map(_.rangeAxisIndex) getOrElse 0)
	jplot.mapDatasetToDomainAxis(sv.datasetIndex, 0);
  }
  
  // ----------------------------------------------------------------------------------------------------
  
  def toBufferedImage(width:Int, height:Int):BufferedImage =  jfree.createBufferedImage(width, height)
  
  def toBufferedImage():BufferedImage = toBufferedImage(chartConfig.width, chartConfig.height)
  
  /**
   * Write the chart into an image file, format is guessed from the filename extension
   * @param filename chosen image filename
   * @param width  generated image width
   * @param height generated image height
   */
  def toFile(filename:String, width:Int, height:Int) {
    val file = chartConfig.destDir map { new File(_,filename)} getOrElse new File(filename)
    file.getName.split("[.]").last.toLowerCase match {
      case "png" => ChartUtilities.saveChartAsPNG(file, jfree, width, height)
      case "jpg"|"jpeg"|_ => ChartUtilities.saveChartAsJPEG(file, jfree, width, height)
    }
  }
  
  /**
   * Write the chart into an image file, format is guessed from the filename extension
   * @param filename chosen image filename
   */
  def toFile(filename:String) {toFile(filename, chartConfig.width, chartConfig.height)}
  
  /**
   * returns series chosen scales
   * @return the rate series
   */
  def scales():Map[Series[Cell], Scale] = seriesViewScale map {case ( sv, scale) => sv.series->scale }
  
  /**
   * returns series chosen colors
   * @return series, color map
   */
  def colors():Map[Series[Cell], Color] = seriesViewList map {sv => sv.series->sv.color} toMap
}
